/*
 * This file is part of the OSMembrane project.
 * More informations under www.osmembrane.de
 * 
 * The project is licensed under the GNU GENERAL PUBLIC LICENSE 3.0.
 * for more details about the license see http://www.osmembrane.de/license/
 * 
 * Source: $HeadURL: http://osmembrane-gui.googlecode.com/svn/sources/src/de/osmembrane/model/pipeline/Pipeline.java $ ($Revision: 905 $)
 * Last changed: $Date: 2011-05-08 14:35:55 +0000 (Sun, 08 May 2011) $
 */


package de.osmembrane.model.pipeline;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Observable;
import java.util.Stack;

import de.osmembrane.model.ModelProxy;
import de.osmembrane.model.algorithms.GraphPlanarizer;
import de.osmembrane.model.algorithms.TarjanAlgorithm;
import de.osmembrane.model.parser.ParserFactory;
import de.osmembrane.model.persistence.AbstractPersistence;
import de.osmembrane.model.persistence.FileException;
import de.osmembrane.model.persistence.FileType;
import de.osmembrane.model.persistence.OSMembranePersistence;
import de.osmembrane.model.persistence.PersistenceFactory;
import de.osmembrane.model.persistence.PipelinePersistenceObject;
import de.osmembrane.model.pipeline.PipelineObserverObject.ChangeType;
import de.osmembrane.model.settings.SettingType;
import de.osmembrane.resources.Constants;
import de.osmembrane.tools.Tools;

/**
 * Implementation of {@link AbstractPipeline}.
 * 
 * @author jakob_jarosch
 */
public class Pipeline extends AbstractPipeline {

	private Stack<PipelineMemento> undoStack;
	private PipelineMemento currentState;
	private Stack<PipelineMemento> redoStack;

	private List<AbstractFunction> functions;

	private URL pipelineFilename;

	private AbstractPipelineSettings pipelineSettings;

	private boolean savedState;

	/**
	 * Says if the pipeline is silent or not.<br/>
	 * In the silent-mode the pipeline will not inform any observers.
	 */
	private boolean silent;

	/**
	 * Says if the pipeline uses undo-redo or not.<br/>
	 * in disabled mode the pipeline does not save any undo redo steps.
	 */
	private boolean undoRedoDisabled;

	/**
	 * Creates a default pipeline with<br/>
	 * 
	 * @link {@link Pipeline#silent} = false
	 */
	public Pipeline() {
		this(false);
	}

	/**
	 * Creates a default pipeline with<br/>
	 * 
	 * @link {@link Pipeline#undoRedoDisabled} = false
	 */
	public Pipeline(boolean silent) {
		this(silent, false);
	}

	/**
	 * Constructor for {@link Pipeline}.
	 */
	public Pipeline(boolean silent, boolean undoRedoDisabled) {
		this.functions = new ArrayList<AbstractFunction>();
		this.undoStack = new Stack<PipelineMemento>();
		this.redoStack = new Stack<PipelineMemento>();
		this.silent = silent;
		this.undoRedoDisabled = undoRedoDisabled;
		this.savedState = true;
		this.pipelineFilename = null;
		this.pipelineSettings = new PipelineSettings();

		/* register the Observer of Persistence to the Pipeline */
		addObserver(PersistenceFactory.getInstance());
	}

	@Override
	public AbstractPipelineSettings getSettings() {
		return pipelineSettings;
	}

	@Override
	public void clear() {
		this.functions.clear();
		this.undoStack.clear();
		this.redoStack.clear();
		pipelineSettings = new PipelineSettings();
		pipelineFilename = null;

		changeSavedState(true);

		/* notify the observers */
		changedNotifyObservers(new PipelineObserverObject(
				ChangeType.FULLCHANGE, null).setCreateUndoStep(false));
	}

	@Override
	public AbstractFunction[] getFunctions() {
		AbstractFunction[] functions = new AbstractFunction[this.functions
				.size()];
		functions = this.functions.toArray(functions);
		return functions;
	}

	@Override
	public void addFunction(AbstractFunction func) {
		func.setPipeline(this);
		func.addObserver(this);
		functions.add(func);

		/* notify the observers */
		changedNotifyObservers(new PipelineObserverObject(
				ChangeType.ADD_FUNCTION, func));
	}

	@Override
	public boolean deleteFunction(AbstractFunction func) {
		boolean returnValue = false;

		for (AbstractFunction function : functions) {
			if (function == func) {
				function.unlinkConnectors();
				returnValue = functions.remove(function);
				break;
			}
		}

		if (returnValue == true) {
			/* notify the observers */
			changedNotifyObservers(new PipelineObserverObject(
					ChangeType.DELETE_FUNCTION, func));
		}

		return returnValue;
	}

	@Override
	public boolean isComplete() {
		/* check all functions */
		for (AbstractFunction function : getFunctions()) {
			if (!function.isComplete()) {
				return false;
			}
		}

		/* all functions seems to be complete, so the pipeline is also complete. */
		return true;
	}

	@Override
	public void loadPipeline(URL filename) throws FileException {
		AbstractPersistence persistence = PersistenceFactory.getInstance()
				.getPersistence(OSMembranePersistence.class);

		PipelinePersistenceObject pipeline = (PipelinePersistenceObject) persistence
				.load(filename);

		clear();

		pipelineFilename = filename;
		this.functions = pipeline.getFunctions();
		for (AbstractFunction function : functions) {
			function.addObserver(this);
			function.setPipeline(this);
		}

		this.pipelineSettings = pipeline.getSettings();

		changeSavedState(true);

		/* notify the observers */
		changedNotifyObservers(new PipelineObserverObject(
				ChangeType.FULLCHANGE, null).setCreateUndoStep(false));
	}

	@Override
	public void savePipeline() throws FileException {
		savePipeline(pipelineFilename);
	}

	@Override
	public void savePipeline(URL filename) throws FileException {
		AbstractPersistence persistence = PersistenceFactory.getInstance()
				.getPersistence(OSMembranePersistence.class);

		persistence.save(filename, new PipelinePersistenceObject(functions,
				pipelineSettings));
		pipelineFilename = filename;

		/* Saved successfully (persistence has not thrown a FileException */
		changeSavedState(true);

		changedNotifyObservers(new PipelineObserverObject(
				ChangeType.SAVED_PIPELINE, null).setCreateUndoStep(false));
	}

	@Override
	public boolean isSaved() {
		return savedState;
	}

	@Override
	public URL getFilename() {
		return pipelineFilename;
	}

	@Override
	public void backupPipeline() throws FileException {
		AbstractPersistence persistence = PersistenceFactory.getInstance()
				.getPersistence(OSMembranePersistence.class);

		persistence.save(Constants.DEFAULT_BACKUP_FILE,
				new PipelinePersistenceObject(functions, pipelineSettings));
	}

	@Override
	public boolean isBackupAvailable() {
		File file = Tools.urlToFile(Constants.DEFAULT_BACKUP_FILE);
		return file.isFile();
	}

	@Override
	public void loadBackup() throws FileException {
		loadPipeline(Constants.DEFAULT_BACKUP_FILE);
	}

	@Override
	public void clearBackup() {
		if (isBackupAvailable()) {
			Tools.urlToFile(Constants.DEFAULT_BACKUP_FILE).delete();
		}
	}

	@Override
	public void importPipeline(URL filename, FileType type)
			throws FileException {
		AbstractPersistence persistence = PersistenceFactory.getInstance()
				.getPersistence(type.getPersistenceClass());

		PipelinePersistenceObject pipeline = (PipelinePersistenceObject) persistence
				.load(filename);

		clear();

		this.functions = pipeline.getFunctions();
		for (AbstractFunction function : functions) {
			function.addObserver(this);
		}

		this.pipelineSettings = pipeline.getSettings();

		/* notify the observers */
		changedNotifyObservers(new PipelineObserverObject(
				ChangeType.FULLCHANGE, null));
	}

	@Override
	public String generate(FileType filetype) {
		return ParserFactory
				.getInstance()
				.getParser(filetype.getParserClass())
				.parsePipeline(
						new PipelinePersistenceObject(functions,
								pipelineSettings));
	}

	@Override
	public void exportPipeline(URL filename, FileType type)
			throws FileException {
		AbstractPersistence persistence = PersistenceFactory.getInstance()
				.getPersistence(type.getPersistenceClass());

		persistence.save(filename, new PipelinePersistenceObject(functions, pipelineSettings));
	}

	@Override
	public boolean hasLoop() {
		TarjanAlgorithm check = new TarjanAlgorithm(functions);
		check.run();

		/* check the SCCs */
		List<List<AbstractFunction>> sccs = check.getSCC();

		for (List<AbstractFunction> scc : sccs) {
			if (scc.size() == 1) {
				/*
				 * check if the scc with size 1 links to itself or is just
				 * standing alone
				 */
				AbstractFunction function = scc.get(0);
				for (AbstractConnector outConnector : function
						.getOutConnectors()) {
					for (AbstractConnector inConnector : outConnector
							.getConnections()) {
						if (inConnector.getParent() == function) {
							/* found a connection to the function itself */
							return true;
						}
					}
				}
			} else if (scc.size() > 1) {
				return true;
			}
		}

		return false;
	}

	@Override
	public void arrangePipeline() {
		GraphPlanarizer gprizer = new GraphPlanarizer(functions);
		gprizer.planarize();
		changedNotifyObservers(new PipelineObserverObject(
				ChangeType.FULLCHANGE, null));
	}

	@Override
	public boolean undo() {
		if (!undoAvailable()) {
			return false;
		}

		redoStack.push(currentState);
		restoreMemento(undoStack.pop());

		return true;
	}

	@Override
	public boolean undoAvailable() {
		return !undoStack.isEmpty();
	}

	@Override
	public boolean redo() {
		if (!redoAvailable()) {
			return false;
		}

		undoStack.push(currentState);
		restoreMemento(redoStack.pop());

		return true;
	}

	@Override
	public boolean redoAvailable() {
		return !redoStack.isEmpty();
	}

	@Override
	public void update(Observable o, Object arg) {
		/* only PipelineObserverObjects will be passed through */
		if (arg instanceof PipelineObserverObject) {
			changedNotifyObservers((PipelineObserverObject) arg);
		}
	}

	@Override
	protected void changedNotifyObservers(PipelineObserverObject poo) {
		poo.setPipeline(this);

		/* check if the undo-step is really required, or disabled. */
		if (poo.createUndoStep() && !undoRedoDisabled) {
			/* any changes made, set savedState to false */
			changeSavedState(false);
		}

		if (!silent) {
			this.setChanged();
			this.notifyObservers(poo);
		}
	}

	private void changeSavedState(boolean state) {
		this.savedState = state;

		if (state == false) {
			saveStep();
		} else {
			/*
			 * Update the savedState for the current item (nothing changed in
			 * the pipeline only the state should be updated.
			 */
			currentState = new PipelineMemento(functions, savedState);
		}
	}

	private void saveStep() {
		undoStack.push(currentState);
		redoStack.clear();
		currentState = new PipelineMemento(functions, savedState);

		int maximumStackSize = ((Integer) ModelProxy.getInstance()
				.getSettings().getValue(SettingType.MAXIMUM_UNDO_STEPS))
				.intValue();

		while (undoStack.size() > maximumStackSize) {
			undoStack.remove(0);
		}
	}

	private void restoreMemento(PipelineMemento memento) {
		this.functions = memento.getFunctions();
		this.savedState = memento.getSavedState();
		this.currentState = memento;

		for (AbstractFunction function : functions) {
			function.addObserver(this);
		}

		changedNotifyObservers(new PipelineObserverObject(
				ChangeType.FULLCHANGE, null).setCreateUndoStep(false));
	}
}

/**
 * Private internal class of the memento of the undo-/redo-feature.
 * 
 * @author jakob_jarosch
 */
class PipelineMemento {

	private List<AbstractFunction> functions = new ArrayList<AbstractFunction>();
	private boolean savedState;

	public PipelineMemento(List<AbstractFunction> functions, boolean savedState) {
		this.functions = deepCopyFunctions(functions);
		this.savedState = savedState;
	}

	public boolean getSavedState() {
		return savedState;
	}

	public List<AbstractFunction> getFunctions() {
		return deepCopyFunctions(functions);
	}

	private List<AbstractFunction> deepCopyFunctions(
			List<AbstractFunction> functions) {
		/* Use serialization to create a copy of the functions in the pipeline */
		try {
			ByteArrayOutputStream baos = new ByteArrayOutputStream(4096);
			ObjectOutputStream oos = new ObjectOutputStream(baos);
			oos.writeObject(functions);
			ByteArrayInputStream bais = new ByteArrayInputStream(
					baos.toByteArray());
			ObjectInputStream ois = new ObjectInputStream(bais);
			Object deepCopy = ois.readObject();

			@SuppressWarnings("unchecked")
			List<AbstractFunction> copy = (List<AbstractFunction>) deepCopy;
			return copy;
		} catch (IOException e) {
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}

		return functions;
	}
}
